﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentDateTime;
using System.ComponentModel;

namespace de.codingfreaks.libs.cfDateTime
{
    /// <summary>
    /// Encapsulates the logic of dealing with date spans.
    /// </summary>
    /// <remarks>
    /// <para>This library makes use of the FluentDateTime-library available at http://fluentdatetime.codeplex.com which is
    /// under Apache License 2.0.</para>
    /// </remarks>
    [DefaultEvent("PropertyChanged")]
    public class DateTimeSpan : INotifyPropertyChanged,
                                IComparable
    {

        #region member vars

        private bool m_blnEnsureCompleteDates;
        private DateTime? m_dtmFrom;
        private DateTime? m_dtmTo;

        #endregion

        #region event declarations

        /// <summary>
        /// Occurs after one of the properties got a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        
        /// <summary>
        /// Occurs if some error occured. Happens before any exception and enables
        /// the caller to prevent the following exception.
        /// </summary>
        public event Delegates.ErrorOccuredEventHandler ErrorOccured;

        #endregion

        #region constructors

        /// <summary>
        /// Default constructor which will set DateFrom to the time 00:00:00 and 
        /// DateTo to 23:59:59.
        /// </summary>
        public DateTimeSpan()
        {
            this.EnsureCompleteDays = true;
            this.DateFrom = DateTime.Now;
            this.DateTo = DateTime.Now;
        }

        /// <summary>
        /// Constructor for defining the both dates.
        /// </summary>
        /// <param name="dtmFrom">The init-value for DateFrom.</param>
        /// <param name="dtmTo">The init-value for DateTo.</param>
        public DateTimeSpan(DateTime dtmFrom,
                            DateTime dtmTo)
        {
            this.DateFrom = dtmFrom;
            this.DateTo = dtmTo;
        }

        /// <summary>
        /// Constructor for defining the both dates and the internal handling of them.
        /// </summary>
        /// <param name="dtmFrom">The init-value for DateFrom.</param>
        /// <param name="dtmTo">The init-value for DateTo.</param>
        /// <param name="blnEnsureCompleteDays">True, if it has to be ensured, that the class always handles complete days.</param>
        public DateTimeSpan(DateTime dtmFrom,
                            DateTime dtmTo,
                            bool blnEnsureCompleteDays) : 
            this(dtmFrom, dtmTo)
        {
            this.EnsureCompleteDays = blnEnsureCompleteDays;
        }

        /// <summary>
        /// Constructor for defining the both dates so that the resulting span
        /// is a complete given year.
        /// </summary>
        /// <param name="intYear">The year which should be bordered by DateFrom and DateTo.</param>
        public DateTimeSpan(int intYear)
        {
            EnsureCompleteDays = true;
            try
            {
                DateFrom = new DateTime(intYear, 1, 1);
                DateTo = new DateTime(intYear, 12, 31);
            }
            catch 
            {
                ArgumentException excThis = new ArgumentException("Can not create valid dates with the given year!");
                if (!OnErrorOccured(excThis, string.Empty))
                {
                    throw excThis;
                }
            }
        }

        /// <summary>
        /// Constructor for defining the both dates so that the resulting span
        /// is a complete given year.
        /// </summary>
        /// <param name="intYear">The year which should be bordered by DateFrom and DateTo.</param>
        public DateTimeSpan(int intYear, 
                            int intMonth)
        {            
            EnsureCompleteDays = true;
            DateFrom = new DateTime(intYear, intMonth, 1);
            DateTo = intMonth.LastDayOfMonth(intYear);
        }

        #endregion

        #region methods

        #region private

        /// <summary>
        /// Is called to ensure the settings of the date-handling will take effect
        /// in the correct way.
        /// </summary>
        private void CheckDates()
        {
            if (m_blnEnsureCompleteDates)
            {
                // We do not use the properties to change the values here, because this
                // method is called by the property-setters and this would end up in endless
                // cyclic calls otherwise.
                if (m_dtmFrom.HasValue)
                    m_dtmFrom = m_dtmFrom.Value.BeginningOfDay();
                if (m_dtmTo.HasValue)
                    m_dtmTo = m_dtmTo.Value.EndOfDay();
            }
        }

        /// <summary>
        /// Fires the ErrorOccured-event.
        /// </summary>
        /// <param name="excThis">The exception if there was one.</param>
        /// <param name="strMessage">The message for the caller.</param>
        /// <returns>True, if a following exception should be thrown.</returns>
        private bool OnErrorOccured(Exception excThis,
                                    string strMessage)
        {
            if (ErrorOccured != null)
            {
                EventArgs.ErrorOccuredEventArgs e = new de.codingfreaks.libs.cfDateTime.EventArgs.ErrorOccuredEventArgs(excThis, strMessage);
                ErrorOccured(this, e);
                return e.DoNotThrowException;
            }
            return false;
        }

        #endregion

        #region public

        /// <summary>
        /// Determines if a given <c>DateTime</c> lays between DateFrom and DateTo.        
        /// </summary>
        /// <param name="dtmCheck">The <c>DateTime</c> to check.</param>
        /// <returns>True, if DateFrom and DateTo are not null and the check-date lays between them.</returns>
        public bool LaysInside(DateTime dtmCheck)
        {
            bool blnRet = false;
            if (DateFrom.HasValue &&
                DateTo.HasValue) 
            {
                // DateFrom and DateTo are set actually
                blnRet = dtmCheck >= DateFrom.Value &&
                         dtmCheck <= DateTo.Value;
            }
            return blnRet;
        }

        /// <summary>
        /// Compares this instance with another <c>DateTimeSpan</c>.
        /// </summary>
        /// <param name="obj">The <c>DateTimeSpan</c> to compare this instance with.</param>
        /// <returns>-1, 0 or 1 depending on the comparison-result.</returns>
        public int CompareTo(object obj)
        {
            if (!(obj is DateTimeSpan))
            {
                ArgumentException excThis = new ArgumentException(string.Format("Can not compare DateTimeSpan with {0}!", obj.GetType().Name));
                if (!OnErrorOccured(excThis, string.Empty))
                {
                    throw excThis;
                }
            }
            return CompleteTimeSpan.CompareTo(((DateTimeSpan)obj).CompleteTimeSpan);
        }

        #endregion

        #region protected

        /// <summary>
        /// Handles the firing of the PropertyChanged-event.
        /// </summary>
        /// <param name="strPropertyName">The name of the property which has changed.</param>
        protected void OnNotifyPropertyChanged(string strPropertyName)
        {
            if (PropertyChanged != null)
            { 
                // at least one listener found for the event
                PropertyChanged(this, new PropertyChangedEventArgs(strPropertyName)); 
            }
        }

        #endregion

        #endregion

        #region properties

        /// <summary>
        /// If set to true, the class will ensure, that the span between the
        /// both dates will contain all minutes and seconds of the days.
        /// </summary>
        public bool EnsureCompleteDays
        {
            get { return m_blnEnsureCompleteDates; }
            set
            {
                m_blnEnsureCompleteDates = value;
                if (m_blnEnsureCompleteDates)
                    CheckDates();
            }
        }

        /// <summary>
        /// Gets/sets the starting date of the span.
        /// </summary>
        /// <remarks>
        /// Has to be smaller than or equal DateTo!
        /// </remarks>
        public DateTime? DateFrom
        {
            get { return m_dtmFrom; }
            set
            {
                if (m_dtmTo.HasValue &&
                    m_dtmTo < value)
                {
                    // to is set and would be smaller than from
                    ArgumentException excThis = new ArgumentException("DateFrom must be smaller than to if to has a value!");
                    if (!OnErrorOccured(excThis, string.Empty))
                    {
                        throw excThis;
                    }
                }
                m_dtmFrom = value;
                OnNotifyPropertyChanged("DateFrom");
                CheckDates();
            }
        }

        /// <summary>
        /// Gets/sets the ending date of the span.
        /// </summary>
        /// <remarks>
        /// Has to be larger than or equal DateFrom.
        /// </remarks>
        public DateTime? DateTo
        {
            get { return m_dtmTo; }
            set
            {
                if (m_dtmFrom.HasValue &&
                    m_dtmFrom > value)
                {
                    // from is set and would be larger than to
                    ArgumentException excThis = new ArgumentException("DateTo must be larger than from if from has a value!");
                    if (!OnErrorOccured(excThis, string.Empty))
                    {
                        throw excThis;
                    }                   
                }
                m_dtmTo = value;
                OnNotifyPropertyChanged("DateTo");
                CheckDates();
            }
        }

        /// <summary>
        /// Returns the <see cref="System.TimeSpan"/> between From and To if both have values.
        /// </summary>
        public TimeSpan CompleteTimeSpan
        {
            get
            {
                if (DateFrom.HasValue &&
                    DateTo.HasValue)
                    return DateTo.Value.Subtract(DateFrom.Value);
                return new TimeSpan(0);
            }
        }

        /// <summary>
        /// Returns true, if FromDate and ToDate are not null and not equal.
        /// </summary>
        public bool HasSpan
        {
            get { return CompleteTimeSpan.Ticks > 0; }
        }

        /// <summary>
        /// Returns a generic list of <c>DateTime</c>-objects containing
        /// all days of the span.
        /// </summary>
        public List<DateTime> Days
        {
            get
            {
                // We will return an empty list if there is no DateFrom and DateTo,
                // but it will be a list in any case!
                List<DateTime> lstRet = new List<DateTime>();
                if (DateFrom.HasValue)
                {
                    // Since DateFrom is not null, the list will at least contain it.
                    lstRet.Add(DateFrom.Value);
                }
                if (DateTo.HasValue)
                {
                    // Since DateTo is not null, the list will at least contain it. We will
                    // only add it after all other values that will possible come, so we don't
                    // have to sort afterwards.
                    if (DateFrom.HasValue)
                    {
                        // So DateFrom AND DateTo are not null. So we will cycle through each day
                        // between them and add it to the return-list.
                        DateTime dtmTmp = DateFrom.Value;
                        while (DateTo.Value.Subtract(dtmTmp).Days > 0)
                        {
                            dtmTmp = dtmTmp.AddDays(1);
                            lstRet.Add(dtmTmp);
                        }
                    } 
                }
                // Return the list, even if it is empty.
                return lstRet;
            }
        }

        #endregion
                
    }
}
